package com.example.demo.aop;

import com.alibaba.fastjson.JSONObject;
import com.example.demo.annotate.NoCheckToken;
import com.example.demo.common.Func;
import com.example.demo.common.Result;
import com.example.demo.common.Valid;
import com.example.demo.common.constant.RedisKey;
import com.example.demo.common.constant.ResultCode;
import com.example.demo.entity.config.LyMode;
import com.example.demo.entity.config.LySecret;
import com.example.demo.entity.config.LyTime;
import com.example.demo.entity.vo.LoginerVO;
import com.example.demo.exception.VisitException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@Order(100)
@RequiredArgsConstructor
@Slf4j
public class SecurityAop {
    private final RedisTemplate<String, String> redis;
    private final LyMode lyMode;
    private final LySecret lySecret;
    private final LyTime lyTime;

    private String newToken = null;

    @Pointcut("execution(public * com.example.demo.controller.*.*(..))")
    public void SecurityAop() {

    }

    @Around("SecurityAop()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 返回值 方法类名 方法名
        // 例：Result com.example.demo.controller.JwtController.getJwtString()
        //Func.print("Signature:" + joinPoint.getSignature());

        // 如果是测试模式或标有NoCheckToken注解的接口，不必检测
        if (!lyMode.isTest() && !isNoCheckToken(joinPoint)) {
            checkJwt();
        }
        /*if (!lyMode.isTest() && !filterKeywords(request)) {
            //checkToken(request);
            checkJwt(request);
            //checkJson(request);
        }*/

        return joinPoint.proceed();
    }

    /*
     * 功能：判断是否旧Token失效，若是则附加新Token，返回给前端
     * 注：
     * 在这里将由于旧Token失效而生成的新Token附加到返回值里，只对于Result类型的返回值有效，需要二次封装的返回值无效
     * 按实际情况再调整附加新newToken的其它位置
     **/
    @AfterReturning(pointcut = "SecurityAop()", returning = "result")
    public void afterReturning(Object result) {
        if (newToken != null) {
            ((Result) result).put("newToken", newToken);
            newToken = null;
        }
    }

    private boolean isNoCheckToken(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        NoCheckToken noCheckToken = method.getAnnotation(NoCheckToken.class);
        return noCheckToken != null;
    }

    /*private boolean filterKeywords() {
        HttpServletRequest request = Func.getHttpRequest();
        String serverPath = request.getServletPath();

        List<String> keywordsList = new ArrayList<>();
        keywordsList.add("/api/user/loginToken");
        keywordsList.add("/api/user/loginJwt");
        keywordsList.add("/api/user/loginJson");
        for (String temp : keywordsList) {
            if (serverPath.contains(temp)) {
                return true;
            }
        }

        return false;
    }*/

    private void checkToken() {
        HttpServletRequest request = Func.getHttpRequest();
        String token = request.getHeader("user_token");

        // redis的实体类没有被注入初始化，出错
        if (redis == null) {
            log.info(ResultCode.USER_REDIS_INIT_ERROR.getMessage());
            throw new VisitException(ResultCode.USER_REDIS_INIT_ERROR);
        }

        // token为空，说明没有经过正常的登录操作
        if (Valid.isEmpty(token)) {
            log.info(ResultCode.USER_TOKEN_WEB_DELETION.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_WEB_DELETION);
        }

        // token：tokenId（32位的UUID）+ 随机字符串（26位的ULID）
        if (token.length() != 58) {
            log.info(ResultCode.USER_TOKEN_LENGTH_ERROR.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_LENGTH_ERROR);
        }

        // 截取前32位的tokenId
        String tokenId = token.substring(0, 32);
        String random = token.substring(32);

        // redis里没有查询到相应的key-value，说明未正常登录，或者过期了请重新登录
        String tokenKey = String.format(RedisKey.LOGIER_TOKEN + "%s", tokenId);
        String tokenValue = redis.opsForValue().get(tokenKey);
        if (Valid.isEmpty(tokenValue)) {
            log.info(tokenId + ":" + ResultCode.USER_TOKEN_REDIS_DELETION.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_REDIS_DELETION);
        }

        // redis里相应的key-value不匹配，说明已在别处登录
        if (!random.equals(tokenValue)) {
            log.info(tokenId + ":" + ResultCode.USER_TOKEN_RANDOM_ERROR.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_RANDOM_ERROR);
        }

        // 判断时间间隔是否超过30分钟，若是，说明过期了，请重新登录
        String timeKey = String.format(RedisKey.LOGIER_TIME + "%s", tokenId);
        String timeValue = redis.opsForValue().get(timeKey);
        long oldTime = Long.parseLong(timeValue);
        long minutes = (System.currentTimeMillis() - oldTime) / (1000 * 60);
        if (minutes > lyTime.getOvertimeMin()) {
            // 删除 logier:name:{id}、logier:role:{id}、logier:time:{id}、logier:token:{id} 的键值
            redis.delete(tokenKey);
            redis.delete(timeKey);
            redis.delete(RedisKey.LOGIER_NAME + tokenId);
            redis.delete(RedisKey.LOGIER_ROLE + tokenId);

            log.info(tokenId + ":" + ResultCode.USER_TOKEN_TIMEOUT.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_TIMEOUT);
        }

        // 判断时间间隔是否超过15分钟，若是，则更新相应键值的有效期
        if (minutes > lyTime.getIntervalMin()) {
            // 更新logier:time:{id}的时间戳
            redis.opsForValue().set(timeKey, String.valueOf(System.currentTimeMillis()));

            // 更新 logier:name:{id}、logier:role:{id}、logier:time:{id}、logier:token:{id} 的有效期
            redis.expire(tokenKey, 40, TimeUnit.MINUTES);
            redis.expire(timeKey, 40, TimeUnit.MINUTES);
            redis.expire(RedisKey.LOGIER_NAME + tokenId, 40, TimeUnit.MINUTES);
            redis.expire(RedisKey.LOGIER_ROLE + tokenId, 40, TimeUnit.MINUTES);

            log.info(tokenId + ":" + ResultCode.USER_TOKEN_EXPIRE.getMessage());
        }

        log.info(tokenId);
    }

    /*private void checkTokenOld() {
        HttpServletRequest request = Func.getHttpRequest();
        String token = request.getHeader("user_token");
        String serverName = Func.getIP(request);
        String serverPath = request.getServletPath();

        //redis的实体类没有被注入初始化，出错
        if (redis == null) {
            log.info(ResultCode.USER_REDIS_INIT_ERROR.getMessage());
            throw new VisitException(ResultCode.USER_REDIS_INIT_ERROR);
        }

        //token为空，说明没有经过正常的登录操作
        if (Valid.isEmpty(token)) {
            log.info(serverName + "," + serverPath + ":" + ResultCode.USER_TOKEN_WEB_DELETION.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_WEB_DELETION);
        }

        //token里的内容项数少于6，出错（6项：用户ID、用户名、密码、姓名、上次操作的时间戳、标识本次登录行为的随机6位数）
        String separate1 = "@#&%", separate2 = "%@#&", separate3 = "&%@#", separate4 = "#&%@";
        String regex = "\\" + separate1 + "|" + separate2 + "|" + separate3 + "|" + separate4;
        String[] temp = token.split(regex);
        if (temp.length != 6) {
            log.info(serverName + "," + serverPath + ":" + ResultCode.USER_TOKEN_LENGTH_ERROR.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_LENGTH_ERROR);
        }

        //token里的6项内容任意一项为空，都不正常
        String userid = temp[0], username = temp[1], password = temp[2], name = temp[3], time = temp[4], random = temp[5];
        if (Valid.isEmpty(userid) || Valid.isEmpty(username) || Valid.isEmpty(password) ||
                Valid.isEmpty(name) || Valid.isEmpty(time) || Valid.isEmpty(random)) {
            log.info(userid + "," + serverName + "," + serverPath + ":" + ResultCode.USER_TOKEN_CONTENT_EMPTY.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_CONTENT_EMPTY);
        }

        //redis里没有查询到相应的key-value，出错
        String key = String.format(RedisKey.LOGIER_TOKEN + "%s", userid);
        String value = Func.decode(redis.opsForValue().get(key));
        if (Valid.isEmpty(value)) {
            log.info(userid + "," + serverName + "," + serverPath + ":" + ResultCode.USER_TOKEN_REDIS_DELETION.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_REDIS_DELETION);
        }

        //判断redis里的userid、username、password是否存在，若不存在，说明该账号的状态已被置为不可用
        String[] list = value.split(regex);
        if (!userid.equals(list[0]) || !username.equals(list[1]) || !password.equals(list[2])) {
            log.info(userid + "," + serverName + "," + serverPath + ":" + ResultCode.USER_TOKEN_CONTENT_ERROR.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_CONTENT_ERROR);
        }

        //若随机6位数不相等，说明已在别处登录
        if (!random.equals(list[4])) {
            log.info(userid + "," + serverName + "," + serverPath + ":" + ResultCode.USER_TOKEN_RANDOM_ERROR.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_RANDOM_ERROR);
        }

        //判断时间间隔是否超过30分钟，若是，说明长时间不操作，须重新登录
        long oldTime = Long.parseLong(list[3]);
        long minutes = (System.currentTimeMillis() - oldTime) / (1000 * 60);
        if (minutes > param.getOvertimeMin()) {
            log.info(userid + "," + serverName + "," + serverPath + ":" + ResultCode.USER_TOKEN_TIMEOUT.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_TIMEOUT);
        }

        //判断时间间隔是否超过15分钟
        if (minutes > param.getIntervalMin()) {
            //进行用户信息的redis验证

            //只更新token中的时间戳
            String newToken = Func.encode(userid + separate1 + username + separate2 + password + separate3 + name + separate4 + new Date().getTime() + separate1 + random);
            redis.opsForValue().set(key, newToken, 36000, TimeUnit.SECONDS);
        }
    }*/

    private void checkTokenNoUserId() {
        HttpServletRequest request = Func.getHttpRequest();
        String token = request.getHeader("user_token");
        String logTokenIdKey = RedisKey.LOG_USERID + MDC.get("traceId");
        String logErrorMsgKey = RedisKey.lOG_ERROR_MSG + MDC.get("traceId");
        int keyExpire = 60;

        // token为空，说明没有经过正常的登录操作
        if (Valid.isEmpty(token)) {
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_WEB_DELETION.getMessage(), keyExpire, TimeUnit.SECONDS);
            throw new VisitException(ResultCode.USER_TOKEN_WEB_DELETION);
        }

        // 截取前32位的tokenId与jwt
        String tokenId = token.substring(0, 32);
        String jwt = token.substring(32);

        // 将tokenId存入redis——>mongodb
        if (Valid.isEmpty(tokenId)) {
            log.info(ResultCode.USER_TOKEN_CONTENT_EMPTY.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_CONTENT_EMPTY.getMessage(), keyExpire, TimeUnit.SECONDS);
            throw new VisitException(ResultCode.USER_TOKEN_CONTENT_EMPTY);
        }
        redis.opsForValue().set(logTokenIdKey, tokenId, keyExpire, TimeUnit.SECONDS);

        // redis里没有查询到相应的key-value，说明未正常登录，或者过期了请重新登录
        String tokenKey = RedisKey.LOGIER + tokenId;
        String tokenValue = redis.opsForValue().get(tokenKey);
        if (Valid.isEmpty(tokenValue)) {
            log.info(tokenId + ":" + ResultCode.USER_TOKEN_REDIS_DELETION.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_REDIS_DELETION.getMessage(), keyExpire, TimeUnit.SECONDS);
            throw new VisitException(ResultCode.USER_TOKEN_REDIS_DELETION);
        }

        // 格式化tokenValue
        LoginerVO loginer = new LoginerVO();
        loginer.setLoginer(JSONObject.parseObject(tokenValue));

        // redis里相应的key-value不匹配，说明已在别处登录
        if (!token.equals(loginer.getToken())) {
            log.info(tokenId + ":" + ResultCode.USER_TOKEN_RANDOM_ERROR.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_RANDOM_ERROR.getMessage(), keyExpire, TimeUnit.SECONDS);
            throw new VisitException(ResultCode.USER_TOKEN_RANDOM_ERROR);
        }

        // 判断时间间隔是否超过30分钟，若是，说明过期了，请重新登录
        long oldTime = loginer.getTime();;
        long minutes = (System.currentTimeMillis() - oldTime) / (1000 * 60);
        if (minutes > lyTime.getOvertimeMin()) {
            // 删除 logier:{id} 的键值
            redis.delete(tokenKey);

            log.info(tokenId + ":" + ResultCode.USER_TOKEN_TIMEOUT.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_TIMEOUT.getMessage(), keyExpire, TimeUnit.SECONDS);
            throw new VisitException(ResultCode.USER_TOKEN_TIMEOUT);
        }

        // 判断时间间隔是否超过15分钟，若是，则更新相应键值的时间，生成新Token并保存到redis
        if (minutes > lyTime.getIntervalMin()) {
            // 生成新token
            createNewTokenNoUserId(tokenId);

            // 更新 logier:{id} 的token、时间戳与有效期
            loginer.setToken(newToken);
            loginer.expire();
            redis.opsForValue().set(tokenKey, JSONObject.toJSONString(loginer), 40, TimeUnit.MINUTES);

            log.info(tokenId + ":" + ResultCode.USER_TOKEN_EXPIRE.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_EXPIRE.getMessage(), keyExpire, TimeUnit.SECONDS);
        }
    }

    private void checkJwt() {
        HttpServletRequest request = Func.getHttpRequest();
        String token = request.getHeader("user_token");
        String logUseridKey = RedisKey.LOG_USERID + MDC.get("traceId");
        String logNameKey = RedisKey.LOG_NAME + MDC.get("traceId");
        String logErrorMsgKey = RedisKey.lOG_ERROR_MSG + MDC.get("traceId");
        int keyExpire = 60;

        // token为空，说明没有经过正常的登录操作
        if (Valid.isEmpty(token)) {
            log.info(ResultCode.USER_TOKEN_WEB_DELETION.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_WEB_DELETION.getMessage(), keyExpire, TimeUnit.SECONDS);
            throw new VisitException(ResultCode.USER_TOKEN_WEB_DELETION);
        }

        // 最好在JWT串加个前缀或后缀，获取时再去掉它，最后再解码（让窃取token值的人误以为这个前缀或后缀也是需要解码的一部分）
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(lySecret.getJwtPassword()).parseClaimsJws(token).getBody();
        }
        catch (Exception ex) {
            log.info(ResultCode.JWT_CHECK_ERROR.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.JWT_CHECK_ERROR.getMessage(), keyExpire, TimeUnit.SECONDS);
            throw new VisitException(ResultCode.JWT_CHECK_ERROR);
        }

        // 获取token里的userid，并将其存入redis——>mongodb
        String userid = claims.get("userid").toString();
        if (Valid.isEmpty(userid)) {
            log.info(ResultCode.USER_TOKEN_CONTENT_EMPTY.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_CONTENT_EMPTY.getMessage(), keyExpire, TimeUnit.SECONDS);
            throw new VisitException(ResultCode.USER_TOKEN_CONTENT_EMPTY);
        }
        redis.opsForValue().set(logUseridKey, userid, keyExpire, TimeUnit.SECONDS);

        // redis里没有查询到相应的key-value，说明未正常登录，或者过期了请重新登录
        String tokenKey = RedisKey.LOGIER + userid;
        String tokenJson = redis.opsForValue().get(tokenKey);
        if (Valid.isEmpty(tokenJson)) {
            log.info(userid + ":" + ResultCode.USER_TOKEN_REDIS_DELETION.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_REDIS_DELETION.getMessage(), keyExpire, TimeUnit.SECONDS);
            throw new VisitException(ResultCode.USER_TOKEN_REDIS_DELETION);
        }

        // 格式化tokenValue，并将用户姓名存入redis——>mongodb
        LoginerVO loginer = new LoginerVO();
        loginer.setLoginer(JSONObject.parseObject(tokenJson));
        redis.opsForValue().set(logNameKey, loginer.getName(), keyExpire, TimeUnit.SECONDS);

        // redis里相应的key-value不匹配，说明已在别处登录
        if (!token.equals(loginer.getToken())) {
            log.info(userid + ":" + ResultCode.USER_TOKEN_RANDOM_ERROR.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_RANDOM_ERROR.getMessage(), keyExpire, TimeUnit.SECONDS);
            throw new VisitException(ResultCode.USER_TOKEN_RANDOM_ERROR);
        }

        // 判断时间间隔是否超过30分钟，若是，说明过期了，请重新登录
        long oldTime = loginer.getTime();;
        long minutes = (System.currentTimeMillis() - oldTime) / (1000 * 60);
        if (minutes > lyTime.getOvertimeMin()) {
            // 删除 logier:{id} 的键值
            redis.delete(tokenKey);

            log.info(userid + ":" + ResultCode.USER_TOKEN_TIMEOUT.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_TIMEOUT.getMessage(), keyExpire, TimeUnit.SECONDS);
            throw new VisitException(ResultCode.USER_TOKEN_TIMEOUT);
        }

        // 判断时间间隔是否超过15分钟，若是，则更新相应键值的时间，生成新Token并保存到redis
        if (minutes > lyTime.getIntervalMin()) {
            // 生成新token
            createNewToken(userid);

            // 更新 logier:{id} 的token、时间戳与有效期
            loginer.setToken(newToken);
            loginer.expire();
            redis.opsForValue().set(tokenKey, JSONObject.toJSONString(loginer), 40, TimeUnit.MINUTES);

            log.info(userid + ":" + ResultCode.USER_TOKEN_EXPIRE.getMessage());
            redis.opsForValue().set(logErrorMsgKey, ResultCode.USER_TOKEN_EXPIRE.getMessage(), keyExpire, TimeUnit.SECONDS);
        }

        // 若无MongoDB存储日志，必须打印userid，否则可不打印（看实际情况而定）
        //log.info(userid);
    }

    private void checkJson() {
        HttpServletRequest request = Func.getHttpRequest();
        String token = request.getHeader("user_token");

        // redis的实体类没有被注入初始化，出错
        if (redis == null) {
            log.info(ResultCode.USER_REDIS_INIT_ERROR.getMessage());
            throw new VisitException(ResultCode.USER_REDIS_INIT_ERROR);
        }

        // token为空，说明没有经过正常的登录操作
        if (Valid.isEmpty(token)) {
            log.info(ResultCode.USER_TOKEN_WEB_DELETION.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_WEB_DELETION);
        }

        // token：tokenId（32位的UUID）+ 随机字符串（26位的ULID）
        if (token.length() != 58) {
            log.info(ResultCode.USER_TOKEN_LENGTH_ERROR.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_LENGTH_ERROR);
        }

        // 截取前32位的tokenId
        String tokenId = token.substring(0, 32);
        String random = token.substring(32);

        // redis里没有查询到相应的key-value，说明未正常登录，或者过期了请重新登录
        String tokenKey = RedisKey.LOGIER + tokenId;
        String tokenJson = redis.opsForValue().get(tokenKey);
        if (Valid.isEmpty(tokenJson)) {
            log.info(tokenId + ":" + ResultCode.USER_TOKEN_REDIS_DELETION.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_REDIS_DELETION);
        }

        // 格式化tokenValue
        LoginerVO loginer = new LoginerVO();
        loginer.setLoginer(JSONObject.parseObject(tokenJson));

        // redis里相应的key-value不匹配，说明已在别处登录
        if (!random.equals(loginer.getToken())) {
            log.info(tokenId + ":" + ResultCode.USER_TOKEN_RANDOM_ERROR.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_RANDOM_ERROR);
        }

        // 判断时间间隔是否超过30分钟，若是，说明过期了，请重新登录
        long oldTime = loginer.getTime();
        long minutes = (System.currentTimeMillis() - oldTime) / (1000 * 60);
        if (minutes > lyTime.getOvertimeMin()) {
            // 删除 logier:{id} 的键值
            redis.delete(tokenKey);

            log.info(tokenId + ":" + ResultCode.USER_TOKEN_TIMEOUT.getMessage());
            throw new VisitException(ResultCode.USER_TOKEN_TIMEOUT);
        }

        // 判断时间间隔是否超过15分钟，若是，则更新相应键值的有效期
        if (minutes > lyTime.getIntervalMin()) {
            // 更新 logier:{id} 的时间戳与有效期
            loginer.expire();
            redis.opsForValue().set(tokenKey, JSONObject.toJSONString(loginer), 40, TimeUnit.MINUTES);

            log.info(tokenId + ":" + ResultCode.USER_TOKEN_EXPIRE.getMessage());
        }

        log.info(tokenId);
    }

    private void createNewToken(String userId) {
        JSONObject head = new JSONObject();
        head.put("alg", SignatureAlgorithm.HS256.getValue());
        head.put("typ", "JWT");

        JSONObject body = new JSONObject();
        body.put("userid", userId);

        newToken = Jwts.builder()
                .setHeader(head)
                .setClaims(body)
                .setId(Func.getULID())
                .signWith(SignatureAlgorithm.HS256, lySecret.getJwtPassword())
                .compact();
    }

    private void createNewTokenNoUserId(String tokenId) {
        JSONObject head = new JSONObject();
        head.put("alg", SignatureAlgorithm.HS256.getValue());
        head.put("typ", "JWT");

        String jwt = Jwts.builder()
                .setHeader(head)
                .setId(Func.getULID())
                .signWith(SignatureAlgorithm.HS256, lySecret.getJwtPassword())
                .compact();

        // 将32位的tokenId（UUID）作为token的前缀
        newToken = tokenId + jwt;
    }
}
